$input v_fragPos, v_normal, v_lightPos, v_shadowCoord

#include "../../ShaderUtils/common.sh"

SAMPLER2D(s_shadowMap, 0);

#define NUM_SAMPLES					32
#define BLOCKER_SEARCH_NUM_SAMPLES	NUM_SAMPLES
#define PCF_NUM_SAMPLES				NUM_SAMPLES
#define NUM_RINGS					10

#define EPS							1e-3
#define PI							3.141592653589793
#define PI2							6.283185307179586

vec2 Samples[NUM_SAMPLES];

float rand_2to1(vec2 uv)
{
	float a = 12.9898, b = 78.233, c = 43758.5453;
	float dt = dot(uv.xy, vec2(a,b)), sn = mod(dt, PI);
	return fract(sin(sn) * c);
}

void createPoissonDiskSamples(vec2 randomSeed)
{
	float ANGLE_STEP = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
	float INV_NUM_SAMPLES = 1.0 / float(NUM_SAMPLES);

	float angle = rand_2to1(randomSeed) * PI2;
	float radius = INV_NUM_SAMPLES;
	float radiusStep = radius;

	for(int i = 0; i < NUM_SAMPLES; i++)
	{
		Samples[i] = vec2(cos(angle), sin(angle)) * pow(radius, 0.75);
		radius += radiusStep;
		angle += ANGLE_STEP;
	}
}

float lit(vec3 lightDir, vec3 fragNormal, vec3 viewDir, float specExponent)
{
	float diffColor = max(0.0, dot(fragNormal, lightDir));
	float specColor = max(0.0, pow(dot(fragNormal, normalize(lightDir + viewDir)), specExponent));

	return diffColor + specColor;
}

float hardShadow(sampler2D shadowMap, vec3 shadowCoord, float bias)
{
	return step(shadowCoord.z - bias, texture2D(shadowMap, shadowCoord.xy).x);
}

float PCF(sampler2D shadowMap, vec3 shadowCoord, float bias, float offset = 2.5)
{
	float texel = offset / 1024.0;
	float visibility = 0.0;

	for (int i = 0; i < NUM_SAMPLES; ++i)
	{
		visibility += hardShadow(shadowMap, vec3(shadowCoord.xy + Samples[i] * texel, shadowCoord.z), bias);
	}

	return visibility / NUM_SAMPLES;
}

float PCSS(sampler2D shadowMap, vec3 shadowCoord, float bias)
{
	float texel = 1.0 / 1024.0;

	if (any(greaterThan(shadowCoord.xy, vec2_splat(1.0))) ||
		any(lessThan   (shadowCoord.xy, vec2_splat(0.0))))
	{
		return 1.0;
	}

	// Find average blocker depth.
	int   allBlockers		= 0;
	float allBlockerDepth	= 0.0;
	float avgBlockerDepth	= 0.0;
	for (int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; ++i)
	{
		float shadowDepth = texture2D(shadowMap, shadowCoord.xy + Samples[i] * texel).x; 
		if (shadowDepth < shadowCoord.z)
		{
			allBlockers++;
			allBlockerDepth += shadowDepth;
		}
	}
	if (allBlockers == 0)
	{
		return 1.0;
	}
	avgBlockerDepth = allBlockerDepth / allBlockers;

	// Calculate penumbra.
	float penumbra = (shadowCoord.z - avgBlockerDepth) / avgBlockerDepth;

	return PCF(shadowMap, shadowCoord, bias, penumbra);
}

float Chebychev(float d, float d2, float t)
{
	float variance	= max(0.00002, d2 - d * d);
	return variance / (variance + pow(t - d, 2.0));
}

float VSM(sampler2D shadowMap, vec3 shadowCoord, float bias)
{
	vec2 moments	= textureLod(shadowMap, shadowCoord.xy, 1.5).xy;
	float p			= step(shadowCoord.z - bias, moments.x);
	float pMax		= Chebychev(moments.x, moments.y, shadowCoord.z);

	float avgBlockerDepth = (moments.x - pMax * shadowCoord.z) / (1.0 - pMax);
	float penumbra = (shadowCoord.z - avgBlockerDepth) / avgBlockerDepth;

	return min(1.0, max(p, pMax));
}

float VSSM(sampler2D shadowMap, vec3 shadowCoord, float bias)
{
	vec2 moments	= texture2D(shadowMap, shadowCoord.xy).xy;
	float p			= step(shadowCoord.z - bias, moments.x);
	float pMax		= Chebychev(moments.x, moments.y, shadowCoord.z);

	float avgBlockerDepth = (moments.x - pMax * shadowCoord.z) / (1.0 - pMax);
	float penumbra = (shadowCoord.z - avgBlockerDepth) / avgBlockerDepth;

	vec2 moments2	= textureLod(shadowMap, shadowCoord.xy, max(1.0, penumbra) * 1.5).xy;
	float p2		= step(shadowCoord.z - bias, moments2.x);
	float pMax2		= Chebychev(moments2.x, moments2.y, shadowCoord.z);

	return min(1.0, max(p2, pMax2));
}

void main()
{
	vec3 tintColor		= vec3_splat(1.0);
	vec3 ambientColor	= vec3_splat(0.1);

	vec3 lightDir		= normalize(v_lightPos);
	vec3 fragNormal		= v_normal;
	vec3 viewDir		= -normalize(v_fragPos);

	vec3 shadowCoord	= (v_shadowCoord.xyz / v_shadowCoord.w + 1.0) / 2.0;
	createPoissonDiskSamples(shadowCoord.xy);
	float visibility	= VSSM(s_shadowMap, shadowCoord, EPS);

	vec3 finalColor		= ambientColor + lit(lightDir, fragNormal, viewDir, 128.0) * tintColor * visibility;

	gl_FragColor		= vec4(finalColor, 1.0);
}